/*
*    swsyd - swsy daemon, make nsswitch easy
*    Copyright (C) 2024  Marcus Pedersén marcus@marcux.org
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package sql

import (
	"fmt"
	"errors"
	"log/slog"
	"slices"
	"strings"
	"time"
	"notabug.org/marcux/swsy/swsyd/common"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)



// Represents the passwd
// file.
type Password struct {
	ID uint32
	Username string `gorm:"unique" form:"username"`
	Password string `gorm:"default:'x'" form:"-"`
	UID int32       `gorm:"unique" form:"uid"`
	GID int32       `form:"gid"`
	Gecos string    `form:"gecos"`
	Home string     `form:"home"`
	Shell string    `form:"shell"`
}

// Represent group file
type Group struct {
	ID uint32
	Groupname string     `gorm:"unique" form:"groupname"`
	GroupPassword string `gorm:"default:'x'" form:"password"`
	GID int32            `gorm:"unique" form:"gid"`
	Users string         `form:"-"`
}


// Represent shadow file
type Shadow struct {
	ID uint32
	Username string       `gorm:"unique" form:"username"`
	Password string       `form:"password"`
	LastChange uint32     `form:"last_change"`
	MinPasswordAge uint32 `gorm:"default:0" form:"min_password_age"`
	MaxPasswordAge uint32 `gorm:"default:99999" form:"max_password_age"`
	WarningPeriod uint32  `gorm:"default:7" form:"warning_period"`
	InactivePeriod uint32 `form:"inactive_period"`
	ExpireDate string     `form:"expire_date"`
}

// Represent host file
type Host struct {
	ID uint32
	IPv4 string        `form:"ipv4"`
	IPv6 string        `form:"ipv6"`
	Hostnames string   `gorm:"not null" form:"hostnames"`
}

// Returns users with specified
// name. If user is empty string
// and uid is a negative value
// all users are returned.
// If uid is a positive value
// specified user is returned.
// If name and search is soecified
// all users starting with name is
// returned. If only name is specified
// user with exactly that name is returned.
func GetUser(db *gorm.DB, name string, uid int32, search bool) []Password {
	users := []Password{}
	if len(name) == 0 && uid < 0 {
		rows, err := db.Find(&[]Password{}).Rows()

		if err != nil {
			slog.Error(fmt.Sprintf("Failed to get all password: %s", err))
			return users
		}
		
		defer rows.Close()

		for rows.Next() {
			var u Password
			db.ScanRows(rows, &u)
			users = append(users, u) 
		}	
	} else if uid >= 0 {
		rows, err := db.Where("UID = ?", uid).Find(&Password{}).Rows()

		if err != nil {
			slog.Error(fmt.Sprintf("Failed to get password with UID: %d, %s", uid, err))
			return users
		}

		defer rows.Close()

		for rows.Next() {
			var u Password
			db.ScanRows(rows, &u)

			users = append(users, u)
		}
	} else if search {
		rows,err := db.Where("username LIKE ?",
			fmt.Sprintf("%s%%", name)).Find(&[]Password{}).Rows()

		if err != nil {
			slog.Error(fmt.Sprintf("Failed to get all users starting with: %s from database", name))
			return users
		}

		defer rows.Close()

		for rows.Next() {
			var u Password
			db.ScanRows(rows, &u)

			users = append(users, u)
		}
	} else {
		rows,err := db.Where("username = ?", name).Find(&Password{}).Rows()

		if err != nil {
			slog.Error(fmt.Sprintf("Failed to get users with username: %s from database", name))
			return users
		}

		defer rows.Close()

		for rows.Next() {
			var u Password
			db.ScanRows(rows, &u)

			users = append(users, u)
		}
	}

	return users
}


// Gets group, group
// specified with either
// groupname or uid.
// If both are provided gid
// will be used, gid must be
// a negative value to not be used.
// Search will return groups starting
// with searched string.
// If search is provided with gid
// it will be ignored.
// If name is empty string, gid is
// negative and search is true
// then all groups are returned.
func GetGroup(db *gorm.DB, name string, gid int32, search bool) []Group {
	groups := []Group{}
	
	if gid >= 0 {
		// Get group with gid
		g := Group{}
		res := db.Where("g_id = ?", gid).First(&g)

		if res.Error != nil {
			slog.Error(fmt.Sprintf("Failed to get group with gid: %d from database: %s", gid, res.Error))
		} else {
			groups = append(groups, g)
		}
	} else if len(name) > 0 && search {
		// Get groups starting with groupnasme.
		rows, err := db.Table("groups").Where("groupname LIKE ?",
			fmt.Sprintf("%s%%", name)).Rows()

		if err != nil {
			slog.Error(fmt.Sprintf("Failed to get group with groupname starting with: %s from database: %s", name, err))
		} else {
			defer rows.Close()

			for rows.Next() {
				g := Group{}
				db.ScanRows(rows, &g)
				groups = append(groups, g)
			}
		}
	} else if len(name) > 0 {
		// Get group with groupname
		g := Group{}
		res := db.Where("groupname = ?", name).First(&g)

		if res.Error != nil {
			slog.Error(fmt.Sprintf("Failed to get group with groupname: %s from database: %s", name, res.Error))
		} else {
			groups = append(groups, g)
		}
	} else if len(name) == 0 && gid < 0 && search {
		rows, err := db.Find(&[]Group{}).Rows()
		if err != nil {
			slog.Error(fmt.Sprintf("Failed to get all groups from database: %s", err))
		} else {
			defer rows.Close()

			for rows.Next() {
				g := Group{}
				db.ScanRows(rows, &g)
				groups = append(groups, g)
			}
		}
	}

	return groups
}




// Gets users group, users
// specified with either
// username or uid.
// If both are provided uid
// will be used, uid must be
// a negative value to not be used.
// First value in returned
// slice is the primary group.
func GetUserGroups(db *gorm.DB, name string, uid int32) []Group {
	groups := []Group{}
	u := Password{}
	
	if uid >= 0 {
		res := db.Where("uid = ?", uid).First(&u)

		if res.Error != nil {
			slog.Error("GetUserGroups: Failed to get user with uid: %s from database: %s", uid, res.Error)
			return groups
		}
	} else if len(name) > 0 {
		res := db.Where("username = ?", name).First(&u)

		if res.Error != nil {
			slog.Error("GetUserGroups: Failed to get user with username: %s from database: %s", name, res.Error)
			return groups
		}
	} else {
		return groups
	}

	rows, err := db.Table("groups").Where("users LIKE ?",
		fmt.Sprintf("%%%s%%", u.Username)).Rows()

	if err != nil {
		slog.Error(fmt.Sprintf("GetUserGroups: Failed to get all groups for user with uid: %s, %s", uid, err))

		return groups
	}

	defer rows.Close()

	var primGroup Group
	
	for rows.Next() {
		var g Group
		db.ScanRows(rows, &g)

		if g.GID == u.GID {
			primGroup = g
		} else {
			usrs := strings.Split(g.Users, ",")

			for _, usr := range usrs {
				if strings.TrimSpace(usr) == u.Username {
					groups = append(groups, g)
					break
				}
			}
		}
	}
	
	if len(primGroup.Groupname) > 0 {
		groups = append([]Group{primGroup}, groups...)
	}

	return groups
}



// Gets shadow fields,
// specified with either
// username or uid.
// If both are provided uid
// will be used, uid must be
// a negative value to not be used.
// Search will return shadow fields
// starting with searched username.
// If search is provided with uid
// it will be ignored.
// If user is empty string, uid is
// negative and search is true
// then all shadow records are returned.
func GetShadow(db *gorm.DB, user string, uid int32, search bool) []Shadow {
	shadows := []Shadow{}

	if uid >= 0 {
		// Get shadow for uid

		u := Password{}

		res := db.Table("passwords").Where("uid = ?", uid).First(&u)

		if res.Error != nil {
			slog.Error("GetShadow: Failed to get user with UID: %d from database: %s", uid, res.Error)
			return shadows
		}

		s := Shadow{}
		res = db.Table("shadows").Where("username = ?", u.Username).First(&s)

		if res.Error != nil {
			slog.Error("GetShadow: Failed to get shadow with username: %s, %s",
				u.Username, res.Error)
		} else {
			shadows = append(shadows, s)
		}
		
	} else if len(user) > 0 && search {
		// Get shadows starting with user
		
		rows, err := db.Table("shadows").Where("username LIKE ?",
			fmt.Sprintf("%%%s%%", user)).Rows()

		if err != nil {
			slog.Error(fmt.Sprintf("GetShadow: Failed to get all shadow fields for user with name: %s, %s", user, err))

			return shadows
		}

		defer rows.Close()

		for rows.Next() {
			s := Shadow{}
			db.ScanRows(rows, &s)

			shadows = append(shadows, s)
		}
	} else if len(user) > 0 {
		// Get shadow for username
		
		s := Shadow{}

		res := db.Table("shadows").Where("username = ?", user).First(&s)

		if res.Error != nil {
			slog.Error("GetShadow: Failed to get shadow with username: %s, %s",
				user, res.Error)
		} else {
			shadows = append(shadows, s)
		}
	} else if len(user) == 0 && uid < 0 && search {
		// Get all shadow fields
		
		rows, err := db.Find(&[]Shadow{}).Rows()

		if err != nil {
			slog.Error("GetShadow: Failed to get all shadow fields")
		} else {
			defer rows.Close()
			
			for rows.Next() {
				s := Shadow{}

				db.ScanRows(rows, &s)
				shadows = append(shadows, s) 
			}
		}
	}

	return shadows
}



// Gets host,
// specified with either
// hostname, ipv4 or ipv6.
// If more then one is provided
// hostname will be used.
// Search will return hosts
// starting with searched hostname.
// If search is provided with any other
// argument, it will be ignored.
// If name, ipv4 and ipv6 are empty string
// and search is true
// then all hosts are returned.
func GetHost(db *gorm.DB, name string, ipv4 string, ipv6 string, search bool) []Host {
	hosts := []Host{}

	if search && len(name) == 0 && len(ipv4) == 0 && len(ipv6) == 0 {
		// Get all hosts
		rows, err := db.Find(&[]Host{}).Rows()

		if err != nil {
			slog.Error(fmt.Sprintf("GetHost: Failed to get all hosts: %s", err))
		} else {
			defer rows.Close()

			for rows.Next() {
				h := Host{}
				db.ScanRows(rows, &h)
				hosts = append(hosts, h)
			}
		}
	} else if len(name) > 0 && !search && len(ipv4) == 0 && len(ipv6) == 0 {
		// Get host with hostname
		h := Host{}

		res := db.Table("hosts").Where("hostnames = ?", name).First(&h)

		if res.Error != nil {
			slog.Error(fmt.Sprintf("GetHost: Failed tyo get host with hostname: %s, %s",
				name, res.Error))
		} else {
			hosts = append(hosts, h)
		}
	} else if len(name) > 0 && search && len(ipv4) == 0 && len(ipv6) == 0 {
		// Get host starting with hostname
		rows, err := db.Table("hosts").Where("hostnames LIKE ?",
			fmt.Sprintf("%%%s%%", name)).Rows()

		if err != nil {
			slog.Error(fmt.Sprintf("GetHost: Failed to get hosts starting with: %s, %s",
				name, err))
		} else {
			defer rows.Close()

			for rows.Next() {
				h := Host{}
				db.ScanRows(rows, &h)
				hosts = append(hosts, h)
			}
		}
	} else if len(ipv4) > 0 && !search && len(name) == 0 && len(ipv6) == 0 {
		// Get host with IPv4
		h := Host{}

		res := db.Table("hosts").Where("ipv4 = ?", ipv4).First(&h)

		if res.Error != nil {
			slog.Error(fmt.Sprintf("GetHost: Fasiled to get host with ipv4: %s, %s",
				ipv4, res.Error))
		} else {
			hosts = append(hosts, h)
		}
	} else if len(ipv6) > 0 && !search && len(ipv4) == 0 && len(name) == 0 {
		// Get host with IPv6
		h := Host{}

		res := db.Table("hosts").Where("ipv6 = ?", ipv6).First(&h)

		if res.Error != nil {
			slog.Error(fmt.Sprintf("GetHost: Failed to get host cwith ipv6: %s, %s",
				ipv6, res.Error))
		} else {
			hosts = append(hosts, h)
		}
	}
	
	return hosts
}


// Insert user in database
func InsertUser(db *gorm.DB, conf common.Config, user Password) error { 
	u := GetUser(db, user.Username, -1, false)

	if len(u) > 0 {
		return fmt.Errorf("Failed to add user: User with username: %s already exist.", user.Username)
	}

	uids := []int32{}
	gids := []int32{}
	
	rows, err := db.Table("passwords").Where("uid >= ?", conf.MinUid).Order("uid").Rows()
	if err != nil {
		return fmt.Errorf("Failed to get all users where Uid >= %d, %s",
			conf.MinUid, err)
	}

	for rows.Next() {
		p := Password{}
		db.ScanRows(rows, &p)
		uids = append(uids, p.UID)
	}

	rows, err = db.Table("groups").Where("g_id >= ?", conf.MinGid).Order("g_id").Rows()

	for rows.Next() {
		g := Group{}
		db.ScanRows(rows, &g)
		gids = append(gids, g.GID)
	}

	var uid, gid int32 = -1, -1

	if conf.MinUid == conf.MinGid {
		no := 0
		for i := uint64(conf.MinUid); no < len(uids); i++ {
			if ! slices.Contains(uids, int32(i)) {
				if ! slices.Contains(gids, int32(i)) {
					uid = int32(i)
					gid = uid
					break
				}
			}
			
			no++
		}

		if uid == -1 {
			if len(uids) > 0 {
				uid = uids[len(uids)-1] + 1
			} else {
				uid = int32(conf.MinUid)
			}
				
			for {
				if ! slices.Contains(gids, uid) {
					gid = uid
					break
				}

				uid++
			}
		}
	} else {
		uid = int32(conf.MinUid)
		for {
			if ! slices.Contains(uids, uid) {
				break
			}

			uid++
		}

		gid = int32(conf.MinGid)
		for {
			if ! slices.Contains(gids,gid) {
				break
			}

			gid++
		}
	}

	user.UID = uid
	user.GID = gid
	res := db.Create(&user)
	if res.Error != nil {
		return fmt.Errorf("Failed to insert user: %s in database, %s", user.Username, res.Error)
	}

	if res.RowsAffected > 1 {
		return fmt.Errorf("Database error, to many rows affected: %d", res.RowsAffected)
	}

	return nil
}



// Insert group in database
func InsertGroup(db *gorm.DB, conf common.Config, group Group) error {
	grp := GetGroup(db, "", group.GID, false)

	if len(grp) > 0 {
		return fmt.Errorf("Failed to add group, group with GID = %d already exist.", group.GID)
	}

	grp = GetGroup(db, group.Groupname, -1, false)
	if len(grp) > 0 {
		return fmt.Errorf("Failed to add group, group with name: %s already exists.", group.Groupname)
	}
		
	if group.GID == 0 {
		gids := []int32{}

		rows, err := db.Table("groups").Where("g_id >= ?", conf.MinGid).Order("g_id").Rows()
		if err != nil {
			return fmt.Errorf("Failed to get all groups with GID >= %d from database: %s", err)
		}

		for rows.Next() {
			g := Group{}
			db.ScanRows(rows, &g)
			gids = append(gids, g.GID)
		}

		no := 0
		gid := int32(0)
		
		for i := uint64(conf.MinGid); no < len(gids); i++ {
			if ! slices.Contains(gids, int32(i)) {
				gid = int32(i)
				break
			}
			
			no++
		}

		if gid == 0 {
			if len(gids) > 0 {
				gid = gids[len(gids)-1]+1
			} else {
				gid = int32(conf.MinGid)
			}
		}

		group.GID = gid
	}

	res := db.Create(&group)
	if res.Error != nil {
		return fmt.Errorf("Failed to insert group: %s in database, %s", group.Groupname, res.Error)
	}

	if res.RowsAffected > 1 {
		return fmt.Errorf("Database error, to many rows affected: %d", res.RowsAffected)
	}

	
	return nil
}



// Insert new shadow in database
// If uid >= 0 it will be used
// otherwise shadow.Username
// will be used
func InsertShadow(db *gorm.DB, conf common.Config, shadow Shadow, uid int32) error {
	unixTime := time.Now().Unix()
	
	shadow.LastChange = uint32(unixTime)

	if uid >= 0 {
		shs := GetUser(db, "", uid, false)

		if len(shs) <= 0 {
			return fmt.Errorf("User with uid: %d does not exist", uid)
		} else if len(shs) > 1 {
			return fmt.Errorf("Wrong number of rows found in database for uid: %d", uid)
		}

		shadow.Username = shs[0].Username
	} else {
		shs := GetUser(db,shadow.Username, -1, false)

		if len(shs) <= 0 {
			return fmt.Errorf("User with username: %s does not exist", shadow.Username)
		} else if len(shs) > 1 {
			return fmt.Errorf("Wrong number of rows found in database for username: %s", shadow.Username)
		}
	}

	exist := GetShadow(db, shadow.Username, -1, false)

	if len(exist) > 0 {
		return fmt.Errorf("Password for username: %s, already exist", shadow.Username)
	}
	
	res := db.Create(&shadow)

	if res.Error != nil {
		return fmt.Errorf("Failed to insert password in database %s", res.Error)
	}

	if res.RowsAffected > 1 {
		return fmt.Errorf("Database error, to many rows affected: %d", res.RowsAffected)
	}
	
	return nil
}


// Insert new host in database
func InsertHost(db *gorm.DB, conf common.Config, host Host) error {
	ip4Exist := GetHost(db, "", host.IPv4, "", false)

	if len(ip4Exist) > 0 {
		return fmt.Errorf("Failed to add host. Host with IPv4 address: %s, already exist.", host.IPv4)
	}

	ip6Exist := GetHost(db, "", "", host.IPv6, false)

	if len(ip6Exist) > 0 {
		return fmt.Errorf("Failed to add host. Host with IPv6 address: %s already exist.", host.IPv6)
	}

	var hExist []Host
	for _, h := range strings.Fields(host.Hostnames) {
		hExist = GetHost(db, h, "", "", true)
		if len(hExist) > 0 {
			return fmt.Errorf("Failed to add host. Hostname: %s already exist", h)
		}
	}

	res := db.Create(&host)

	if res.Error != nil {
		return fmt.Errorf("Failed to insert host in database %s", res.Error)
	}

	if res.RowsAffected > 1 {
		return fmt.Errorf("Database error, to many rows affected: %d", res.RowsAffected)
	}
	
	return nil
}


// Init database and return handle
func InitDb(c common.Config) (*gorm.DB, error) {
	db, err := gorm.Open(sqlite.Open(c.Dbfile), &gorm.Config{})
	if err != nil {
		return &gorm.DB{}, errors.New(
			fmt.Sprintf("Failed to open database: %s, %s\n",
				"swsy.db", err),
		)
	}

	db.AutoMigrate(&Password{})
	db.AutoMigrate(&Group{})
	db.AutoMigrate(&Shadow{})
	db.AutoMigrate(&Host{})

	return db, nil
}
